
// load Alkaline
if (args[0] == "release")
	eval(readFile('Alkaline.js'));
else {
	eval(readFile('Alkaline.bootstrap.js'));
	var require = function(filename) {
		require.files.push(filename);
	}
	require.files = [];
	Alkaline.bootstrap(this);
	for (var i = 0; i < require.files.length; i++)
		eval(readFile(require.files[i]));
}

var failed = [];

var K = function(){};

var separator = '--------------------';

var testIdx = 0;

var checkModules = function(input, isFileName) {
	var compileResult = new Alkaline.CompileResult(K, K, K);
	if (isFileName)
		compileResult.inputSource.addSource(input);
	else
		compileResult.inputSource.addCode(input, 'input');
	compileResult.outputModule.setArgument('o', null);	// do not write to file
	compileResult.addModule('5to3');
	compileResult.addModule('Optimize');
	compileResult.addModule('Minify');
	try {
		if (!compileResult.run()) {
			echo(separator);
			echo('Failed to fully process test ' + testIdx);
			echo('Input:')
			echo(input);
			failed.push(testIdx);
			echo(separator);
		}
	} catch(e) {
		echo(separator);
		failed.push(testIdx);
		echo('Crashed when checking modules with test ' + testIdx + '.');
		echo('Error: ' + e);
		echo('Input:');
		echo(input);
		echo(separator);
	}
}

// check test result to see if it matches expected output
var checkResults = function (compileResult, expected) {
	var real = [].concat(compileResult.errors).concat(compileResult.warnings);
	var msg = separator + '\nTest ' + testIdx + ' failed.\n\nExpected: \n' + expected.join('\n') + '\n\nReceived:\n' + real.join('\n') + '\n';
	if (real.length != expected.length) {
		echo(msg);
		return false;
	}
	nextMessage:
	for (var i = 0; i < real.length; i++) {
		var cur = real[i].match(/\d+/g);
		for (var j = 0; j < expected.length; j++)
			if (expected[j][0] == cur[0] && expected[j][1] == cur[1] && expected[j][2] == cur[2] && real[i].indexOf(' ' + compileResult.inputSource.files[expected[j][3]].filename + ':') > -1) {
				expected.splice(j, 1);
				continue nextMessage;
			}
		echo(msg);
		return false;
	}
	return true;
}
	
// run a test and then compare the result to the expected pattern; tests compile errors and warnings
var doTest = function (input, expected) {
	testIdx++;
	try {
		var compileResult = new Alkaline.CompileResult(K, K, K);
		for (var i = 0; i < input.length; i++)
			if (/[\w\.\/\*]+\.(\w+|\*)$/.test(input[i]))
				compileResult.inputSource.addSource(input[i]);
			else
				compileResult.inputSource.addCode(input[i], 'code block');
		compileResult.addModule('analyzer');
		if (compileResult.stage1())
			compileResult.stage2();	
		
		if (!checkResults(compileResult, expected)) {
			failed.push(testIdx);
			echo('Input was:');
			echo(input.join('\n'));
			echo(separator);
		}
	} catch (e) {
		echo(separator + '\nTest ' + testIdx + ' crashed with exception: ' + e.message + '\n');
		failed.push(testIdx + ' crashed');
		echo('Input was:');
		echo(input.join('\n'));
		echo(separator);
	}
}
	
// run a test and then compare the result to the expected pattern
var testPrinter = function (input, expectedPretty, expectedShort) {
	testIdx++;
	try {
		var compileResult = new Alkaline.CompileResult(K, K, K);
		compileResult.inputSource.addCode(input, 'input');
		compileResult.outputModule.setArgument('o', null);	// do not write to file
		compileResult.stage1();
		compileResult.stage2();
		compileResult.stage3();
		var realShort = compileResult.outputModule.outputText || "";
		compileResult.outputModule.setArgument('pp');	// pretty print
		compileResult.stage3();
		var realPretty = compileResult.outputModule.outputText || "";
		var fail = false;
		if (expectedPretty != realPretty) {
			fail = true;
			echo(separator + '\nTest ' + testIdx + ' failed.\n');
			echo('Input was:');
			echo(input);
			
			echo('\nExpected prettyPrint:');
			echo(expectedPretty);
			echo('\nActual prettyPrint:');
			echo(realPretty);
		}
		if (expectedShort != realShort) {
			if (!fail) {
				fail = true;
				echo(separator + '\nTest ' + testIdx + ' failed.\n');
				echo('Input was:');
				echo(input);
			}
			echo('\nExpected short print:');
			echo(expectedShort);
			echo('\nActual short print:');
			echo(realShort);
		}
		if (fail) {
			failed.push(testIdx);
			echo('\nAST:');
			echo(compileResult.AST.toStringTree());
			echo(separator);
		} else
			checkModules(input);
	} catch (e) {
		echo(separator + '\nTest ' + testIdx + ' crashed with exception: ' + e.message + '\n');
		failed.push(testIdx + ' crashed');
		echo('Input was:');
		echo(input);
		echo(separator);
	}
}

var test5to3 = function(input, expectedPretty) {
	testIdx++;
	try {
		var compileResult = new Alkaline.CompileResult(K, K, K);
		compileResult.inputSource.addCode(input, 'input');
		compileResult.outputModule.setArgument('o', null);	// do not write to file
		compileResult.outputModule.setArgument('pp');	// pretty print
		compileResult.addModule('5to3');
		compileResult.run();
		var realPretty = compileResult.output;
		if (expectedPretty != realPretty) {
			failed.push(testIdx);
			echo(separator + '\nTest ' + testIdx + ' failed.\n');
			echo('Input was:');
			echo(input);
			echo('\nExpected output:');
			echo(expectedPretty);
			echo('\nActual output:');
			echo(realPretty);
			echo('\nAST:');
			echo(compileResult.AST.toStringTree());
		} else
			checkModules(input);
	} catch (e) {
		echo(separator + '\nTest ' + testIdx + ' crashed with exception: ' + e.message + '\n');
		failed.push(testIdx + ' crashed');
		echo('Input was:');
		echo(input);
		echo(separator);
	}
}

var testMinify = function(input, expectedPretty) {
	testIdx++;
	try {
		var compileResult = new Alkaline.CompileResult(K, K, K);
		compileResult.inputSource.addCode(input, 'input');
		compileResult.outputModule.setArgument('o', null);	// do not write to file
		compileResult.outputModule.setArgument('pp');	// pretty print
		compileResult.addModule('Minify');
		compileResult.run();
		var realPretty = compileResult.output;
		if (expectedPretty != realPretty) {
			failed.push(testIdx);
			echo(separator + '\nTest ' + testIdx + ' failed.\n');
			echo('Input was:');
			echo(input);
			echo('\nExpected output:');
			echo(expectedPretty);
			echo('\nActual output:');
			echo(realPretty);
			echo('\nAST:');
			echo(compileResult.AST.toStringTree());
			echo(separator);
		} else
			checkModules(input);
	} catch (e) {
		echo(separator + '\nTest ' + testIdx + ' crashed with exception: ' + e.message + '\n');
		failed.push(testIdx + ' crashed');
		echo('Input was:');
		echo(input);
		echo(separator);
	}
}

var testOptimize = function(input, expectedPretty) {
	testIdx++;
	try {
		var compileResult = new Alkaline.CompileResult(K, K, K);
		compileResult.inputSource.addCode(input, 'input');
		compileResult.outputModule.setArgument('o', null);	// do not write to file
		compileResult.outputModule.setArgument('pp');	// pretty print
		compileResult.addModule('Optimize');
		compileResult.run();
		var realPretty = compileResult.output;
		if (expectedPretty.indexOf('\n') < 0)
			realPretty = realPretty.replace(/\s+/g, ' ');
		if (expectedPretty !== realPretty) {
			failed.push(testIdx);
			echo(separator + '\nTest ' + testIdx + ' failed.\n');
			echo('Input was:');
			echo(input);
			echo('\nExpected output:');
			echo(expectedPretty);
			echo('\nActual output:');
			echo(realPretty);
			echo('\nAST:');
			echo(compileResult.AST.toStringTree());
			echo(separator);
		} else
			checkModules(input);
	} catch (e) {
		echo(separator + '\nTest ' + testIdx + ' crashed with exception: ' + e.message + '\n');
		failed.push(testIdx + ' crashed');
		echo('Input was:');
		echo(input);
		echo(separator);
	}
}

var testOptimizations = function(list) {
	for (var i = 0; i < list.length; i++)
		testOptimize(list[i][0], list[i][1]);
}

var testOptionsOnce = function(options, input, expectedPretty) {
	testIdx++;
	try {
		var compileResult = new Alkaline.CompileResult(K, K, K);
		compileResult.inputSource.addCode(input, 'input');
		compileResult.outputModule.setArgument('o', null);	// do not write to file
		compileResult.outputModule.setArgument('pp');	// pretty print
		//compileResult.addModule('Optimize');
		for (var i = 0; i < options.length; i++)
			compileResult.addModule(options[i]);
		compileResult.run();
		var realPretty = compileResult.output;
		if (expectedPretty.indexOf('\n') < 0)
			realPretty = realPretty.replace(/\s+/g, ' ');
		if (expectedPretty !== realPretty) {
			failed.push(testIdx);
			echo(separator + '\nTest ' + testIdx + ' failed.\n');
			echo('Input was:');
			echo(input);
			echo('\nExpected output:');
			echo(expectedPretty);
			echo('\nActual output:');
			echo(realPretty);
			echo('\nAST:');
			echo(compileResult.AST.toStringTree());
			/*
			for (var i = 0; i < expectedPretty.length; i++)
				if (expectedPretty.charAt(i) !== realPretty.charAt(i)) {
					echo(i + ': ' + expectedPretty.charCodeAt(i) + '/' + realPretty.charCodeAt(i));
					echo(expectedPretty.substr(i));
					break;
				}
			*/
			echo(separator);
		} else
			checkModules(input);
	} catch (e) {
		echo(separator + '\nTest ' + testIdx + ' crashed with exception: ' + e.message + '\n');
		failed.push(testIdx + ' crashed');
		echo('Input was:');
		echo(input);
		echo(separator);
	}
}

var testOptions = function(options, list) {
	for (var i = 0; i < list.length; i++)
		testOptionsOnce(options, list[i][0], list[i][1]);
}

var testParse = function(filename, skipWarnings) {
	testIdx++;
	try {
		var compileResult = new Alkaline.CompileResult(K, echo, K);
		compileResult.inputSource.addSource(filename);
		echo(compileResult.inputSource.input.length + ' characters');
		var d = new Date().valueOf();
		if (!compileResult.stage1()) {
			failed.push(testIdx);
			echo('Failed to parse ' + filename + ' (' + compileResult.errors.length + ' errors).');
		} else {
			d = new Date().valueOf() - d;
			echo('Done in ' + ((d/1000)|0) + ' seconds.');
			if (compileResult.warnings.length)
				echo(compileResult.warnings.length + ' warning(s).');
			
			// print to string
			compileResult.outputModule.setArgument('o', null);	// do not write to file
			compileResult.outputModule.setArgument('pp');	// pretty print
			compileResult.stage2();
			d = new Date().valueOf();
			compileResult.stage3();
			d = new Date().valueOf() - d;
			echo('Printed in ' + ((d/1000)|0) + ' seconds.');
			var output = compileResult.outputModule.outputText;
			
			// re-parse printed string
			var recompileResult = new Alkaline.CompileResult(K, echo, K);
			recompileResult.inputSource.addCode(output);
			d = new Date().valueOf();
			if (!recompileResult.stage1()) {
				failed.push(testIdx);
				echo('Failed to re-parse ' + filename + ' after printing (' + recompileResult.errors.length + ' errors).');
			} else {
				d = new Date().valueOf() - d;
				echo('Re-parsed in ' + ((d/1000)|0) + ' seconds.');
				recompileResult.stage2();
				recompileResult.stage3();	// account for AST changes caused by the printer
				var ast1 = compileResult.AST.toStringTree().replace(/'/g, '"');	// replace() because selectors are modified from (SELECT a 'b') to (SELECT a "b")
				var ast2 = recompileResult.AST.toStringTree().replace(/'/g, '"');
				if (ast1 !== ast2) {
					echo("FAILED: Parse trees do not match!");
					echo("Original tree:");
					echo(ast1);
					echo("Re-parsed tree:");
					echo(ast2);
					failed.push(testIdx);
				} else
					echo("Parse trees match");
			}
		}
	} catch (e) {
		echo(separator + '\nCRASH while parsing ' + filename + ' with exception: ' + e.message + '\n');
		failed.push(testIdx + ' crashed');
	}
}

var files = {};
var dirs = {};

var realReadFile = readFile;
var realListFiles = listFiles;
var hijackFileAccess = function() {
	// hijack readFile and listFiles so they return the data we feed them without accessing the filesystem
	readFile = function (filename, silentFail) {
		filename = filename.replace(/\/\.\//g, '/');
		if (files[filename])
			return files[filename];
		if (silentFail)
			return null;
		return '';
	}
	listFiles = function (folder, extension, prefix) {
		var rx = null;
		if (arguments.length > 1) rx = new RegExp('\\.' + extension + '$');
		if (!dirs[folder]) return [];
		var vec = [];
		for (var i = 0; i < dirs[folder].length; i++)
			if (!rx || rx.test(dirs[folder][i]))
				vec.push(dirs[folder][i]);
		return vec;
	}
}
var restoreFileAccess = function() {
	readFile = realReadFile;
	listFiles = realListFiles;
}

//
//	GUnit tests
//

var testSrc;
// the print function is used by Parser->emmitErrorMessage()
var printed = null;
var print = function(msg){
	printed += msg + '\n';
};

var getMatch = function(rx) {
	var m = testSrc.match(rx);
	if (m) {
		testSrc = testSrc.substring(m[0].length);
		var skippedLines = (m[0].match(/\n/g) || []).length;
		line += skippedLines;
		if (skippedLines)
			chr = m[0].match(/\n[^\n]*$/).length-1;
		else
			chr += m[0].length;
	}
	return m;
}

var getTestResult = function(rule, code) {
	var cstream = new org.antlr.runtime.ANTLRStringStream(code);
	var lexer = new AlkalineLexer(cstream);
	var result;
	if (rule.charAt(0) == rule.charAt(0).toUpperCase()) {
		// lexer test
		try {
			result = lexer['m' + rule]() || '';
			if (lexer.state.syntaxErrors)
				result = false;
			else if (lexer.input.index() < lexer.input.size())
				// not all input was consumed
				result = false;
		} catch(e) {
			if (e instanceof org.antlr.runtime.RecognitionException)
				return false;
			return e;
		}
	} else {
		// parser test
		var tstream = new org.antlr.runtime.CommonTokenStream(lexer);
		var parser = new AlkalineParser(tstream);
		try {
			result = parser[rule]() || '';
			if (parser.state.syntaxErrors)
				result = false;
			else if (parser.input.index() < parser.input.size())
				// not all input was consumed
				result = false;
			else
				result = result.tree.toStringTree();
		} catch(e) {
			if (e instanceof org.antlr.runtime.RecognitionException)
				return false;
			else if (e instanceof org.antlr.runtime.tree.RewriteCardinalityException)
				return false;
			return e;
		}
	}
	return result;
}

var gunitTest = function(testSrc1) {
	testSrc = testSrc1;
	// normalize newlines
	testSrc = testSrc.replace(/\r\n/g, '\n').replace(/\n\r/g, '\n').replace(/\r/g, '\n');
	
	line = 1, chr = 1;
	
	var m = getMatch(/^\s*gunit\s+(\w+)\s*;/);
	grammar = m[1];
	//echo('executing test suite for grammar:' + grammar);
	
	var rule;
	var ruleRx = /^(\w+):/;
	var wsRx = /^(\s+|\/\/[^\n]*|\/\*[\s\S]*?\*\/)+/;
	var testRx = /^(?:<<\n((?:[^\n]|\n[^>])*)\n>>|"((?:[^"\\]|\\.)*)")/;
	var resultBoolRx = /^\s+(OK|FAIL)/;
	var resultTreeRx = /^\s+->\s*([^\n]+)/;


	while (true) {
		var testLine, testChr;
		if (m = getMatch(wsRx));
		testLine = line; testChr = chr;
		if (m = getMatch(ruleRx)) {
			rule = m[1];
		} else if (m = getMatch(testRx)) {
			testIdx++;
			var curTest = m[1] || m[2] || '';
			if (m[2]) {
				// fix \ escapes
				curTest = curTest.replace(/\\"/g, '"');
				curTest = curTest.replace(/\\./g, function(_) { return eval('"' + _ + '"'); });
			}
			printed = "";
			var result = getTestResult(rule, curTest);
			if (result instanceof Error) {
				// test crashed
				failed.push(testIdx);
				echo(separator);
				echo('Test ' + testIdx + ' at line ' + testLine + ' CRASHED with exception: ' + result.message);
				echo('Rule ' + rule + '.');
				echo('Input:');
				echo(curTest);
				echo(separator);
				getMatch(resultBoolRx) || getMatch(resultTreeRx);	// dispose of result
			} else if (m = getMatch(resultBoolRx)) {
				// OK/FAIL result
				if (result === '') result = true;
				if ((m[1] == 'FAIL') != (result == false)) {
					failed.push(testIdx);
					echo(separator);
					echo('Test ' + testIdx + ' at line ' + testLine + ' failed. Expected ' + m[1] + ' but test returned ' + (result? 'OK' : 'FAIL'));
					echo('Input length: ' + curTest.length);
					if (curTest.length < 50)
						echo('Test input:\n' + curTest);
					if (printed)
						echo('Test output:\n' + printed);
					echo(separator);
				}
			} else {
				m = getMatch(resultTreeRx);
				// Tree result
				if (result != m[1]) {
					failed.push(testIdx);
					echo(separator);
					echo('Test ' + testIdx + ' at line ' + testLine + ' failed.\nExpected: ' + m[1] + '\nReturned: ' + result);
					echo(separator);
				}
			}
		} else {
			if (testSrc.length)
				echo('Failed to parse gunit file, line ' + line + ', char ' + chr + '.');
			break;
		}
	}
}

//
//	Run the actual unit tests
//

var runTest = function(code) {
	try {
		eval(code);
	} catch(e) {
		failed.push('CRASHED');
		echo('');
		echo('!! Test runner CRASHED when executing test.' + (testIdx ? ' Problem in test runner or no tests in file?' : ' Syntax error?') + ' !!');
		echo('');
	}
}

var totalTests = 0;
var testingStart = new Date().valueOf();

var testToRun = args.shift();
var doTestParse = testToRun === 'testParse';
if (doTestParse)
	testToRun = args.shift();
while (testToRun) {
	testIdx = 0;
	if (doTestParse) {
		echo("Parsing " + testToRun);
		runTest('testParse(testToRun);');
		echo('');
		totalTests++;
	} else {
		echo("Running test " + testToRun);
		var f = readFile(testToRun);
		hijackFileAccess();
		if (testToRun.match(/\.gunit$/))
			gunitTest(f);
		else
			runTest(f);
		restoreFileAccess();
		totalTests += testIdx;
		echo('(' + testIdx + ' test(s) attempted)');
		}
	testToRun = args.shift();
}
	
if (failed.length)
	echo(separator);
	
echo('Total tests ran: ' + totalTests + ', Failures: ' + failed.length);
echo('Done in ' + (new Date().valueOf() - testingStart) + 'ms.');
